package org.bobdawg.app.MALforAndroid;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedList;

import org.apache.http.util.ByteArrayBuffer;
import org.json.JSONArray;
import org.json.JSONObject;

import com.google.android.apps.analytics.GoogleAnalyticsTracker;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.Html;
import android.util.Log;
import android.widget.Toast;

public class MALManager extends IntentService {

	/** push single anime record to mal */
	public final static String PUSH = "org.bobdawg.app.MALforAndroid.PUSH";

	/** pull single anime record from mal */
	public final static String PULL = "org.bobdawg.app.MALforAndroid.PULL";

	/** push all dirty to MAL then pull all from MAL */
	public final static String SYNC = "org.bobdawg.app.MALforAndroid.SYNC";

	/** add new anime to DB and MAL then PULLS it */
	public final static String ADD = "org.bobdawg.app.MALforAndroid.ADD";

	/** deletes record from MAL and DB */
	public final static String REMOVE = "org.bobdawg.app.MALforAndroid.REMOVE";

	/** updates record with new settings */
	public final static String CHANGE = "org.bobdawg.app.MALforAndroid.CHANGE";

	/** pulls image */
	public final static String IMAGE = "org.bobdawg.app.MALforAndroid.IMAGE";

	/** schedules next sync */
	public final static String SCHEDULE = "org.bobdawg.app.MALforAndroid.SCHEDULE";

	/** signals change to underlying data */
	public final static String RELOAD = "org.bobdawg.app.MALforAndroid.RELOAD";
	
	/** fetch all records from MAL, populating DB and caching images */
	public final static String PRECACHEINFO = "org.bobdawg.app.MALforAndroid.PRECACHEINFO";
	
	/** clears the image cache */
	public final static String CLEARCACHE = "org.bobdawg.app.MALforAndroid.CLEARCACHE";

	/** logging definition */
	private final static String LOG_NAME = "MALManager.java";

	private String user;
	private String pass;
	private String api;
	private String cred;
	
	static GoogleAnalyticsTracker tracker;

	private boolean activeConnection;

	public MALManager() {
		super(LOG_NAME);
	}

	/**
	 * 
	 * @param user
	 * @param pass
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private static String encodeCred(String user, String pass) {
		Class<?> b64;
		Object retobj = null;
		try {
			/*
			 * this whole mess is equivalent to
			 * cred = Base64.encodeToString((user + ":" + pass).getBytes(), Base64.NOWRAP); 
			 * on version 2.2 of the android api
			 * but if not available it will default to the 
			 * Robert Harder implementation.
			 */
			b64 = Class.forName("android.util.Base64");
			Class param[] = new Class[2];
			param[0] = pass.getBytes().getClass();
			param[1] = Integer.TYPE;
			Method m = b64.getMethod("encodeToString", param);

			Field f = b64.getField("NO_WRAP");

			Object arglist[] = new Object[2];
			arglist[0] = (user + ":" + pass).getBytes();
			// arglist[1] = new Integer(0x2);
			arglist[1] = f.getInt(null);
			retobj = m.invoke(null, arglist);

			// cred = (String)retobj;
			Log.i("MALManager", "reflection android.util.Base64 found");
		} catch (Exception e) {
			retobj = Base64.encodeBytes((user + ":" + pass).getBytes());
		}
		return (String) retobj;
	}

	@Override
	protected void onHandleIntent(Intent intent) {
		// setup analytics
		tracker = GoogleAnalyticsTracker.getInstance();
		tracker.startNewSession("UA-25348834-2", 300, this);
		
		// track page view
		tracker.trackPageView("/MALManager.java");
		
		String s = intent.getAction();
		Bundle b = intent.getExtras();
		MALRecord ar = null;
		if (b != null) {
			ar = (MALRecord) b.getSerializable("media");
		}

		MALSqlHelper openHelper = new MALSqlHelper(this.getBaseContext());
		SQLiteDatabase db = openHelper.getWritableDatabase();

		SharedPreferences perfs = PreferenceManager.getDefaultSharedPreferences(this);
		user = perfs.getString("userName", "");
		pass = perfs.getString("passwd", "");
		api  = perfs.getString("api", "");

		cred = encodeCred(user, pass);
		// cred = Base64.encodeToString((user + ":" + pass).getBytes(),
		// Base64.DEFAULT | Base64.NOWRAP);

		ConnectivityManager connect = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
		activeConnection = (connect.getNetworkInfo(0).isConnected() || connect.getNetworkInfo(1).isConnected());

		Log.i(LOG_NAME, s);

		if (s.equals(PUSH)) {
			tracker.trackEvent(
					"MALManager", 
					"PUSH", 
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), 
					1);
			push(db, ar);
		} else if (s.equals(PULL)) {
			tracker.trackEvent(
					"MALManager", 
					"PULL", 
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), 
					1);
			pull(db, ar);
		} else if (s.equals(SYNC)) {
			tracker.trackEvent(
					"MALManager", 
					"SYNC", 
					"SYNC", 
					1);
			pushDirty(db);
			sync(db, true);
			sync(db, false);
		} else if (s.equals(ADD)) {
			tracker.trackEvent(
					"MALManager", 
					"ADD", 
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), 
					1);
			add(db, ar);
		} else if (s.equals(REMOVE)) {
			tracker.trackEvent(
					"MALManager", 
					"REMOVE", 
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), 
					1);
			remove(db, ar);
		} else if (s.equals(CHANGE)) {
			tracker.trackEvent(
					"MALManager", 
					"CHANGE", 
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), 
					1);
			change(db, ar);
		} else if (s.equals(IMAGE)) {
			tracker.trackEvent(
					"MALManager", // Category
					"PULL IMAGE", // Action
					((ar instanceof AnimeRecord)?"ANIME":"MANGA") + " ID: " + String.valueOf(ar.id), // Label 
					1);	// Value
			pullImage(db, ar);
		} else if (s.equals(SCHEDULE)) {
			tracker.trackEvent(
					"MALManager", 
					"SCHEDULE", 
					"SCHEDULE", 
					1);
			schedule(true);
		} else if (s.equals(PRECACHEINFO)) {
			tracker.trackEvent(
					"MALManager",
					"PRECACHEINFO",
					"PRECACHEINFO",
					1);
			pushDirty(db);
			preCacheInfo(db, true);
			preCacheInfo(db, false);
		} else {
			tracker.trackEvent(
					"MALManager", 
					"Unknown Intent: " + s, 
					"INFO: Unknown Intent: " + s, 
					0);
			Log.i("MALManager", "unknown intent: " + s);
		}
		db.close();
	}

	/**
	 * push single anime record to mal
	 * @param db
	 * @param ar
	 */
	private void push(SQLiteDatabase db, MALRecord ar) {
		if (activeConnection) {
			
			long id = ar.id;
			StringBuffer sb = new StringBuffer();
			URL url;

			Boolean isAnime = (ar instanceof AnimeRecord);

			try {
				if (isAnime) {
					url = new URL("http://" + api + "/animelist/anime/" + String.valueOf(id));
				} else {
					url = new URL("http://" + api + "/mangalist/manga/" + String.valueOf(id));
				}

				HttpURLConnection con = (HttpURLConnection) url.openConnection();
				con.setDoOutput(true);
				con.setReadTimeout(getResources().getInteger(R.integer.readTimeout));
				con.setConnectTimeout(getResources().getInteger(R.integer.connectTimeout));
				con.setRequestMethod("PUT");
				con.setRequestProperty("Authorization", "Basic " + cred);

				// sb.append( "_method=PUT\n" );
				sb.append("status=").append(ar.watchedStatus);
				if (isAnime) {
					sb.append("&").append("episodes=").append(String.valueOf(((AnimeRecord) ar).watchedEpisodes));
				} else {
					sb.append("&").append("chapters=").append(String.valueOf(((MangaRecord) ar).chaptersRead));
					sb.append("&").append("volumes=").append(String.valueOf(((MangaRecord) ar).volumesRead));
				}
				sb.append("&").append("score=").append(String.valueOf(ar.score));

				OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream());

				wr.write(sb.toString());
				wr.flush();
				wr.close();

				if (200 == con.getResponseCode()) {
					ar.dirty = AnimeRecord.CLEAN;
					ar.pushToDB(db);
				}
			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Push: " + e, 
						0);
				errorNotification();
				Log.e(LOG_NAME, "push", e);
			}
		}
	}

	/**
	 * pull single anime record from mal
	 * @param db
	 * @param ar
	 */
	private void pull(SQLiteDatabase db, MALRecord ar) {
		if (activeConnection) {
			if (ar != null) {
				long id = ar.id;
				URL url;
				try {
					// http://mal-api.com/anime/53?format=xml&mine=1
					if (ar instanceof AnimeRecord) {
						url = new URL("http://" + api + "/anime/" + String.valueOf(id) + "?mine=1");
					} else {
						url = new URL("http://" + api + "/manga/" + String.valueOf(id) + "?mine=1");
					}

					HttpURLConnection con = (HttpURLConnection) url.openConnection();
					con.setReadTimeout(getResources().getInteger(R.integer.readTimeout));
					con.setConnectTimeout(getResources().getInteger(R.integer.connectTimeout));
					con.setRequestProperty("Authorization", "Basic " + cred);

					BufferedReader rd = new BufferedReader(new InputStreamReader(con.getInputStream()), 512);
					String line;
					StringBuffer sb = new StringBuffer();
					while ((line = rd.readLine()) != null) {
						sb.append(line);
					}
					rd.close();

					JSONObject raw = new JSONObject(sb.toString());

					MALRecord newRec;
					if (ar instanceof AnimeRecord) {
						newRec = new AnimeRecord(raw);
					} else {
						newRec = new MangaRecord(raw);
					}

					newRec.pushToDB(db);
					reloadSignal();

					// int fred = con.getResponseCode();
				} catch (Exception e) {
					tracker.trackEvent(
							"MALManager", 
							"ERROR: " + e, 
							"ERROR: Pull: " + e, 
							0);
					errorNotification();
					Log.e(LOG_NAME, "pull", e);
				}
			} else {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: Null Bundle", 
						"ERROR: Pull: Null Bundle", 
						0);
				Log.e(LOG_NAME, "null bundle");
			}
		}
	}	

	/**
	 * push all dirty to MAL then pull all from MAL
	 * 
	 * @param db 	({@code SQLiteDatabase}) 
	 * @param anime	({@code boolean}) if {@code true}, {@code db} contains <code>{@link AnimeRecord}</code>s 
	 */
	// private void syncAnime(SQLiteDatabase db) {
	private void sync(SQLiteDatabase db, boolean anime) {
		if (activeConnection) {
			NotificationManager mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
			Intent intent = new Intent(this, main.class);
			PendingIntent pi = PendingIntent.getActivity(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);

			Notification notification = new Notification(R.drawable.ic_stat_notify_mal_sync, "Synchronizing", System.currentTimeMillis());
			//Notification notification = new Notification(R.drawable.ic_stat_mal, "Synchronizing", System.currentTimeMillis());
			notification.setLatestEventInfo(this, getString(R.string.app_name), getString(R.string.pullAnime), pi);

			notification.flags |= Notification.FLAG_NO_CLEAR;

			mManager.notify(0, notification);

			try {

				URL url;
				if (anime) {
					url = new URL("http://" + api + "/animelist/" + user);
				} else {
					url = new URL("http://" + api + "/mangalist/" + user);
				}

				BufferedReader rd = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream()), 512);
				String line;
				StringBuffer sb = new StringBuffer();
				while ((line = rd.readLine()) != null) {
					sb.append(line);
				}
				rd.close();

				JSONObject raw = new JSONObject(sb.toString());
				JSONArray array;
				if (anime) {
					array = raw.getJSONArray("anime");
				} else {
					array = raw.getJSONArray("manga");
				}

				LinkedList<Long> pulledIds = new LinkedList<Long>();

				for (int i = 0; i < array.length(); i++) {
					JSONObject jo = array.getJSONObject(i);

					notification.setLatestEventInfo(this, this.getString(R.string.app_name) + ": " + String.valueOf(i + 1) + "/" + String.valueOf(array.length()),
							Html.fromHtml(jo.getString("title")).toString(), pi);
					mManager.notify(0, notification);

					MALRecord arNew;
					if (anime) {
						arNew = new AnimeRecord(jo);
					} else {
						arNew = new MangaRecord(jo);
					}

					pulledIds.add(new Long(arNew.id));
					try {
						MALRecord arOld;
						if (anime) {
							arOld = new AnimeRecord(arNew.id, db);
						} else {
							arOld = new MangaRecord(arNew.id, db);
						}

						if (arOld.synopsis != null) {
							//pullImage(db, arNew);
							arNew.synopsis = arOld.synopsis;
							arNew.rank = arOld.rank;
							arNew.memberScore = arOld.memberScore;
						}
						//pull(db, arNew);
						//pullImage(db, arNew);

						if (!arNew.equals(arOld)) {
							arNew.pushToDB(db);
							reloadSignal();
						}
					} catch (Exception e) {
						arNew.pushToDB(db);
						reloadSignal();
					}

				}

				Cursor cur;
				if (anime) {
					cur = db.rawQuery("select id from animeList", null);
				} else {
					cur = db.rawQuery("select id from mangaList", null);
				}

				if (cur.moveToFirst()) {
					while (!cur.isAfterLast()) {
						Long temp = cur.getLong(cur.getColumnIndex("id"));
						if (!pulledIds.contains(temp)) {
							if (anime) {
								db.execSQL("delete from animeList where id = ".concat(temp.toString()));
							} else {
								db.execSQL("delete from mangaList where id = ".concat(temp.toString()));
							}
							reloadSignal();
						}
						cur.moveToNext();
					}
				}

				cur.close();
				mManager.cancelAll();

			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Sync: Failed (" + e + ")", 
						0);
				mManager.cancelAll();
				errorNotification();
				Log.e(LOG_NAME, "Sync failed", e);
			}

		}
		// schedule();
	}
	
	/**
	 * add new anime to DB and MAL then PULLS it
	 * @param db
	 * @param ar
	 */
	private void add(SQLiteDatabase db, MALRecord ar) {
		String ws = ar.watchedStatus;
		if (activeConnection) {
			try {
				URL url;
				if (ar instanceof AnimeRecord) {
					url = new URL("http://" + api + "/animelist/anime");
				} else {
					url = new URL("http://" + api + "/mangalist/manga");
				}

				HttpURLConnection con = (HttpURLConnection) url.openConnection();
				con.setDoOutput(true);
				con.setReadTimeout(getResources().getInteger(R.integer.readTimeout));
				con.setConnectTimeout(getResources().getInteger(R.integer.connectTimeout));
				con.setRequestMethod("POST");
				con.setRequestProperty("Authorization", "Basic " + cred);

				// sb.append( "_method=PUT\n" );
				StringBuffer sb = new StringBuffer();
				if (ar instanceof AnimeRecord) {
					sb.append("anime_id=");
				} else {
					sb.append("manga_id=");
				}
				sb.append(String.valueOf(ar.id));
				sb.append("&status=").append(ar.watchedStatus);

				OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream());

				wr.write(sb.toString());
				wr.flush();
				wr.close();

				int fred = con.getResponseCode();
				if (fred == 200) {
					pull(db, ar);
					ar.pullFromDB(ar.id, db);
					ar.watchedStatus = ws;
					ar.pushToDB(db);
					reloadSignal();
				}

			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Add: " + e, 
						0);
				errorNotification();
				Log.e(LOG_NAME, "add", e);
			}
		}
	}

	/**
	 * deletes record from MAL and DB
	 * @param db
	 * @param ar
	 */
	private void remove(SQLiteDatabase db, MALRecord ar) {
		ar.dirty = AnimeRecord.DELETED;
		ar.pushToDB(db);
		reloadSignal();
		if (activeConnection) {
			try {
				URL url;
				File root = Environment.getExternalStorageDirectory();
				File file;
				if (ar instanceof AnimeRecord) {
					url = new URL("http://" + api + "/animelist/anime/" + String.valueOf(ar.id));
					file = new File(root, getString(R.string.imagePathAnime) + String.valueOf(ar.id));
				} else {
					url = new URL("http://" + api + "/mangalist/manga/" + String.valueOf(ar.id));
					file = new File(root, getString(R.string.imagePathManga) + String.valueOf(ar.id));
				}

				HttpURLConnection con = (HttpURLConnection) url.openConnection();
				con.setReadTimeout(getResources().getInteger(R.integer.readTimeout));
				con.setConnectTimeout(getResources().getInteger(R.integer.connectTimeout));
				con.setRequestMethod("DELETE");
				con.setRequestProperty("Authorization", "Basic " + cred);

				con.connect();

				if (con.getResponseCode() == 200) {
					if (ar instanceof AnimeRecord) {
						db.execSQL("delete from animeList where id = " + String.valueOf(ar.id));
					} else {
						db.execSQL("delete from mangaList where id = " + String.valueOf(ar.id));
					}
				}
				// delete image from cache
				if (file.exists()) {
					file.delete();
				}
			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Remove: " + e, 
						0);
				errorNotification();
				Log.e(LOG_NAME, "remove", e);
			}
		}
	}

	/**
	 * updates record with new settings
	 * @param db
	 * @param ar
	 */
	private void change(SQLiteDatabase db, MALRecord ar) {
		ar.dirty = AnimeRecord.UNSYNCED;
		ar.pushToDB(db);
		if (activeConnection) {
			push(db, ar);
		}
		reloadSignal();
	}

	/**
	 * pulls image
	 * @param db
	 * @param ar
	 */
	private void pullImage(SQLiteDatabase db, MALRecord ar) {
		if (activeConnection) {
			try {
				URL url = new URL(ar.imageUrl);
				URLConnection ucon = url.openConnection();

				File root = Environment.getExternalStorageDirectory();
				File file;
				if (ar instanceof AnimeRecord) {
					// file = new File(root,
					// "Android/data/com.riotopsys.MALForAndroid/images/anime/"
					// + String.valueOf(ar.id));
					file = new File(root, getString(R.string.imagePathAnime) + String.valueOf(ar.id));
				} else {
					// file = new File(root,
					// "Android/data/com.riotopsys.MALForAndroid/images/manga/"
					// + String.valueOf(ar.id));
					file = new File(root, getString(R.string.imagePathManga) + String.valueOf(ar.id));
				}
				file.mkdirs();
				if (file.exists()) {
					file.delete();
					//return;
				}
				file.createNewFile();

				ByteArrayBuffer baf = new ByteArrayBuffer(50);

				InputStream is = ucon.getInputStream();

				BufferedInputStream bis = new BufferedInputStream(is, 1024 * 10);

				int current = 0;

				while ((current = bis.read()) != -1) {

					baf.append((byte) current);

				}

				/* Convert the Bytes read to a String. */
				FileOutputStream fos = new FileOutputStream(file);
				fos.write(baf.toByteArray());
				fos.close();
				reloadSignal();
			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Pull Image: " + e, 
						0);
				errorNotification();
				Log.e(LOG_NAME, "Failed on img", e);
			}
		}
	}

	/**
	 * 
	 * @param db
	 */
	private void pushDirty(SQLiteDatabase db) {
		HttpURLConnection con;
		URL url;

		Cursor c = db.rawQuery("select id, 1 as type from animeList where dirty <> 0 union select id, 2 as type from mangaList where dirty <> 0", null);

		if (c.moveToFirst()) {
			while (!c.isAfterLast()) {

				try {
					MALRecord ar;

					if (c.getInt(c.getColumnIndex("type")) == 1) {
						ar = new AnimeRecord(c.getInt(c.getColumnIndex("id")), db);
					} else {
						ar = new MangaRecord(c.getInt(c.getColumnIndex("id")), db);
					}

					switch (ar.dirty) {
						case AnimeRecord.CLEAN:
							Log.e(LOG_NAME, "WTF I said no 0");
							break;
						case AnimeRecord.UPDATING:
							// only seen during an update
							break;
						case AnimeRecord.UNSYNCED:
							// just a change
							push(db, ar);
							break;
						case AnimeRecord.DELETED:
							// delete
							try {

								url = new URL("http://" + api + "/animelist/anime/" + String.valueOf(ar.id));

								con = (HttpURLConnection) url.openConnection();
								con.setReadTimeout(getResources().getInteger(R.integer.readTimeout));
								con.setConnectTimeout(getResources().getInteger(R.integer.connectTimeout));
								con.setRequestMethod("DELETE");
								con.setRequestProperty("Authorization", "Basic " + cred);

								con.connect();

								if (con.getResponseCode() == 200) {
									db.execSQL("delete from animeList where id = " + String.valueOf(ar.id));
								}

							} catch (Exception e) {
								tracker.trackEvent(
										"MALManager", 
										"ERROR: " + e, 
										"ERROR: Push Dirty: " + e, 
										0);
								errorNotification();
								Log.e(LOG_NAME, "push dirty", e);
							}
							break;
					}
					c.moveToNext();
				} catch (Exception e) {
					tracker.trackEvent(
							"MALManager", 
							"ERROR: " + e, 
							"ERROR: Push Dirty: ar/mr not found (" + e + ")", 
							0);
					Log.e(LOG_NAME, "push dirty: ar not found", e);
				}
			}
		}

		c.close();
		reloadSignal();
	}

	/**
	 * signals change to underlying data
	 */
	private void reloadSignal() {
		Intent i = new Intent(RELOAD);
		sendBroadcast(i);
	}

	/**
	 * schedules next sync
	 * @param createNew
	 */
	private void schedule(boolean createNew) {
		SharedPreferences perfs = PreferenceManager.getDefaultSharedPreferences(this);
		AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);

		int interval = Integer.parseInt(perfs.getString("updateFreq", "14400000"));

		long firstTime = SystemClock.elapsedRealtime() + interval;

		Intent i = new Intent(this, MALManager.class);
		i.setAction(SYNC);
		PendingIntent mAlarmSender = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_NO_CREATE);

		if (interval != -1) {

			if (mAlarmSender == null) {
				mAlarmSender = PendingIntent.getService(this, 0, i, 0);

				am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, interval, mAlarmSender);

				Log.i(LOG_NAME, "schedule set");
			} else if (createNew) {

				am.cancel(mAlarmSender);

				am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, interval, mAlarmSender);

			}
		} else {
			am.cancel(mAlarmSender);
		}
	}
	
	/**
	 * push all dirty to MAL then pull all from MAL
	 * 
	 * @param db 	({@code SQLiteDatabase}) 
	 * @param anime	({@code boolean}) if {@code true}, {@code db} contains <code>{@link AnimeRecord}</code>s 
	 */
	// private void syncAnime(SQLiteDatabase db) {
	private void preCacheInfo(SQLiteDatabase db, boolean anime) {
		if (activeConnection) {
			NotificationManager mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
			Intent intent = new Intent(this, main.class);
			PendingIntent pi = PendingIntent.getActivity(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);

			Notification notification = new Notification(R.drawable.ic_stat_notify_mal_sync, "Synchronizing", System.currentTimeMillis());
			//Notification notification = new Notification(R.drawable.ic_stat_mal, "Synchronizing", System.currentTimeMillis());
			notification.setLatestEventInfo(this, getString(R.string.app_name), getString(R.string.pullAnime), pi);

			notification.flags |= Notification.FLAG_NO_CLEAR;

			mManager.notify(0, notification);

			try {

				URL url;
				if (anime) {
					url = new URL("http://" + api + "/animelist/" + user);
				} else {
					url = new URL("http://" + api + "/mangalist/" + user);
				}

				BufferedReader rd = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream()), 512);
				String line;
				StringBuffer sb = new StringBuffer();
				while ((line = rd.readLine()) != null) {
					sb.append(line);
				}
				rd.close();

				JSONObject raw = new JSONObject(sb.toString());
				JSONArray array;
				if (anime) {
					array = raw.getJSONArray("anime");
				} else {
					array = raw.getJSONArray("manga");
				}

				LinkedList<Long> pulledIds = new LinkedList<Long>();

				for (int i = 0; i < array.length(); i++) {
					JSONObject jo = array.getJSONObject(i);

					notification.setLatestEventInfo(this, this.getString(R.string.app_name) + ": " + String.valueOf(i + 1) + "/" + String.valueOf(array.length()),
							Html.fromHtml(jo.getString("title")).toString(), pi);
					mManager.notify(0, notification);

					MALRecord arNew;
					if (anime) {
						arNew = new AnimeRecord(jo);
					} else {
						arNew = new MangaRecord(jo);
					}

					pulledIds.add(new Long(arNew.id));
					try {
						MALRecord arOld;
						if (anime) {
							arOld = new AnimeRecord(arNew.id, db);
						} else {
							arOld = new MangaRecord(arNew.id, db);
						}

						if (arOld.synopsis != null) {
							//pullImage(db, arNew);
							arNew.synopsis = arOld.synopsis;
							arNew.rank = arOld.rank;
							arNew.memberScore = arOld.memberScore;
						}
						pull(db, arNew);
						pullImage(db, arNew);

						if (!arNew.equals(arOld)) {
							arNew.pushToDB(db);
							reloadSignal();
						}
					} catch (Exception e) {
						arNew.pushToDB(db);
						reloadSignal();
					}

				}

				Cursor cur;
				if (anime) {
					cur = db.rawQuery("select id from animeList", null);
				} else {
					cur = db.rawQuery("select id from mangaList", null);
				}

				if (cur.moveToFirst()) {
					while (!cur.isAfterLast()) {
						Long temp = cur.getLong(cur.getColumnIndex("id"));
						if (!pulledIds.contains(temp)) {
							if (anime) {
								db.execSQL("delete from animeList where id = ".concat(temp.toString()));
							} else {
								db.execSQL("delete from mangaList where id = ".concat(temp.toString()));
							}
							reloadSignal();
						}
						cur.moveToNext();
					}
				}

				cur.close();
				mManager.cancelAll();

			} catch (Exception e) {
				tracker.trackEvent(
						"MALManager", 
						"ERROR: " + e, 
						"ERROR: Sync: Failed (" + e + ")", 
						0);
				mManager.cancelAll();
				errorNotification();
				Log.e(LOG_NAME, "Sync failed", e);
			}

		}
		// schedule();
	}

	/**
	 * 
	 * @param id
	 * @param c
	 * @return
	 */
	public static AnimeRecord getAnime(long id, Context c) {

		MALSqlHelper openHelper = new MALSqlHelper(c);
		SQLiteDatabase db = openHelper.getReadableDatabase();

		AnimeRecord ar;
		try {
			ar = new AnimeRecord(id, db);
		} catch (Exception e) {
			ar = null;
		}

		db.close();
		return ar;
	}

	/**
	 * 
	 * @param id
	 * @param c
	 * @return
	 */
	public static MangaRecord getManga(long id, Context c) {

		MALSqlHelper openHelper = new MALSqlHelper(c);
		SQLiteDatabase db = openHelper.getReadableDatabase();

		MangaRecord ar;
		try {
			ar = new MangaRecord(id, db);
		} catch (Exception e) {
			ar = null;
		}

		db.close();
		return ar;
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public static boolean verifyCredentials(Context c) {
		SharedPreferences perfs = PreferenceManager.getDefaultSharedPreferences(c);
		String user = perfs.getString("userName", "");
		String api = perfs.getString("api", "");
		String pass = perfs.getString("passwd", "");

		String cred = encodeCred(user, pass);
		// String cred = Base64.encodeToString((user + ":" + pass).getBytes(),
		// Base64.DEFAULT | Base64.NO_WRAP);

		Boolean result = false;

		try {
			URL url = new URL("http://" + api + "/account/verify_credentials");

			HttpURLConnection con = (HttpURLConnection) url.openConnection();
			con.setReadTimeout(R.integer.readTimeout /* 10000 milliseconds */);
			con.setConnectTimeout(R.integer.connectTimeout /* 15000 milliseconds */);
			con.setRequestProperty("Authorization", "Basic " + cred);

			con.connect();

			result = (con.getResponseCode() == 200);

		} catch (Exception e) {
			tracker.trackEvent(
					"MALManager", 
					"ERROR: " + e, 
					"ERROR: Verify Credentials: " + e, 
					0);
			Toast.makeText(c, R.string.connectError, Toast.LENGTH_SHORT).show();
			Log.e(LOG_NAME, "verifyCredentials", e);
		}

		return result;

	}

	/**
	 * 
	 */
	private void errorNotification() {
		NotificationManager mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
		Intent intent = new Intent(this, Preferences.class);
		PendingIntent pi = PendingIntent.getActivity(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);

		Notification notification = new Notification(R.drawable.ic_stat_notify_mal_sync_error, getResources().getString(R.string.connectError), System.currentTimeMillis());
		//Notification notification = new Notification(android.R.drawable.stat_notify_error, getResources().getString(R.string.connectError), System.currentTimeMillis());
		notification.setLatestEventInfo(this, this.getString(R.string.app_name), getResources().getString(R.string.connectError), pi);

		mManager.notify(0, notification);
	}

}
